package cs236369.webserver;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Map.Entry;

import javax.xml.xpath.XPathExpressionException;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpException;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpMessage;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpVersion;
import org.apache.http.StatusLine;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.DefaultHttpServerConnection;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.message.BasicStatusLine;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.HttpDateGenerator;
import cs236369.webserver.typehandler.Session;
import cs236369.webserver.typehandler.TypeHandler;

import Logging.Monitor;

public class WebServer {

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

	/**
	 * 
	 * @author artem This class represent a handler that handles request from
	 *         clients this class generates responses to the client
	 * 
	 */

	public class RequestHandler implements Runnable {

		private Request request;
		private Map<String, String> params;
		private String relativePath;

		public RequestHandler(Request _request) {
			request = _request;
			params = new HashMap<String, String>();
		}

		@Override
		public void run() {
			// check if file/directory exist
			File file = null;
			try {
				int lastCharOfPath = parsedConfig.getBaseDirectory().length() - 1;
				String addSlash = ((request.getPath().toCharArray()[0] != '/') && (parsedConfig
						.getBaseDirectory().toCharArray()[lastCharOfPath] != '/')) ? "/"
						: "";
				addSlash = ((request.getPath().toCharArray()[0] == '/') && (parsedConfig
						.getBaseDirectory().toCharArray()[lastCharOfPath] == '/')) ? "."
						: "";
				Monitor.write(this,"The path is:"+request.getPath());
				
				int pathLastCharPos = request.getPath().lastIndexOf('?');
				pathLastCharPos = (pathLastCharPos == -1) ? request.getPath()
						.length() : pathLastCharPos;

				relativePath = request.getPath().substring(0,pathLastCharPos);
				String filePath = parsedConfig.getBaseDirectory() + addSlash + relativePath;
				Monitor.write(this, "Opening file:"+filePath);
				filePath = filePath.replace("\\", System.getProperty("file.separator"));
				filePath = filePath.replace("/", System.getProperty("file.separator"));
				file = new File(filePath);
				String absoluteFilePath = file.getCanonicalPath();
				String absoluteBasePath = (new File(parsedConfig.getBaseDirectory())).getCanonicalPath();
				Monitor.write(this, "The file path:"+absoluteFilePath+
									" and the absolute:"+absoluteBasePath);
				if (absoluteFilePath.indexOf(absoluteBasePath) != 0){
					generateErrorPage(403);
				}
			} catch (XPathExpressionException e) {
				generateErrorPage(500);
				throw new RuntimeException(e.getMessage());
			} catch (IOException e) {
				generateErrorPage(404);
			}
			if ((file == null) || (!file.exists()) || (!file.canRead())) {
				generateErrorPage(404);
			} else {
				// split GET POST handling

				if (request.getHttpMethod().equals("GET")) {

					try {
						handleGet(file);
					} catch (XPathExpressionException e) {
						e.printStackTrace();
						generateErrorPage(500);
						return;
					}
				} else {
					try {
						handlePost(file);
					} catch (XPathExpressionException e) {
						throw new RuntimeException(e.getMessage());
					}
				}
			}

			try {
				request.getSocket().close();
			} catch (IOException e) {
				e.printStackTrace();
				throw new RuntimeException(e.getMessage());
			}
		}

		private void generateErrorPage(int errorCode) {
			StatusLine statusLine = new BasicStatusLine(HttpVersion.HTTP_1_1,
					errorCode, (errorCode == 404) ? "File Not Found"
							: (errorCode == 400) ? "Bad Request"
									: "Internal Server Error");
			String doctype = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional" +
							"//EN\"\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";
			String errorPage = doctype+"<html xmlns=\"http://www.w3.org/1999/xhtml\"><head><title>Error " + errorCode
					+ "</title></head>";
			errorPage += "<body><h1>Error " + errorCode + "</h1>";
			errorPage += "</br></hr><br/> A team of well trained advarcks has been dispatched to deal with the problem";
			errorPage += "</body></html>";
			generateHttpResponseHtml(statusLine, errorPage.getBytes(), true,
					null, "text/html");
		}

		private void sendResponseToClient(HttpResponse httpResponse) {
			Socket socket = request.getSocket();
			HttpParams params = new BasicHttpParams();
			DefaultHttpServerConnection serverConn = new DefaultHttpServerConnection();
			try {
				serverConn.bind(socket, params);
				serverConn.sendResponseHeader(httpResponse);
				serverConn.sendResponseEntity(httpResponse);
			} catch (HttpException e) {
				e.printStackTrace();
				throw new RuntimeException(e.getMessage());
			} catch (IOException e) {
				e.printStackTrace();
				throw new RuntimeException(e.getMessage());
			}
		}

		private void generateFileListPage(File file) throws XPathExpressionException {
			String[] files = file.list();
			String host = request.getHttpHeaders().get("host");
			int dotPos = host.indexOf(":");
			if (dotPos > -1)
				host = host.substring(0, dotPos);
			String doctype = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional" +
							"//EN\"\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";
			String fileListPage = doctype+"<html xmlns=\"http://www.w3.org/1999/xhtml\"><head><title>" + 
								file.getPath()
								+ " files</title></head>";
			fileListPage += "<body><h1>" + file.getPath()
					+ "</h1></br></hr></br></br><ul>";
			for (int i = 0; i < files.length; i++) {
				fileListPage += "<li>";
				String slash1 = (relativePath.toCharArray()[0] == '/')? "":"/";
				String slash2 = (relativePath.toCharArray()[relativePath.length()-1] == '/')? "":"/";
				fileListPage += "<a href = \"http://" + host + ":" + parsedConfig.getPort() + slash1 + relativePath + slash2 + files[i]
						+ " \" alt = \"Directory File\">";
				fileListPage += /* host + */files[i] + "</a>";
				fileListPage += "</li>";
			}
			fileListPage += "</ul></body></html>";
			StatusLine statusLine = new BasicStatusLine(HttpVersion.HTTP_1_1,
					200, "OK");
			generateHttpResponseHtml(statusLine, fileListPage.getBytes(),
					false, null, "text/html");
		}

		private void generateHttpResponseHtml(StatusLine statusLine,
				byte[] content, boolean isError, Integer sessionId,
				String contentType) {
			HttpResponse httpResponse = new BasicHttpResponse(statusLine);
			HttpEntity httpEntity = new ByteArrayEntity(content.clone());
			if (sessionId != null) {
				httpResponse.addHeader("Set-Cookie",
						"sessionId = " + sessionId.toString() + ";");
			}
			httpResponse.addHeader("Date",
					new HttpDateGenerator().getCurrentDate());
			httpResponse.addHeader("Connection", "close");
			httpResponse.addHeader(HttpHeaders.CONTENT_LENGTH,
					Long.toString(httpEntity.getContentLength()));
			httpResponse.addHeader(HttpHeaders.CONTENT_TYPE, contentType);
			httpResponse.setEntity(httpEntity);
			sendResponseToClient(httpResponse);
		}

		private void handlePost(File file) throws XPathExpressionException {
			// check if path is to a specificly handeled type
			// if so, handle request
			String fileName = file.getName();
			int dotPos = fileName.lastIndexOf('.');
			String fileExtension = fileName.substring((dotPos == -1) ? 0
					: dotPos + 1);
			for (Entry<String, String> entry : parsedConfig.getExtensions()
					.entrySet()) {
				if (fileExtension.equals(entry.getKey().toString())) {
					String line = request.getContent();
					if(line.length() > 0){
						parseParams(line);
					}
					handleSpecificType(entry.getValue(), file);
					return;
				}
			}
			// else send error page 400
			generateErrorPage(400);
		}

		// TODO when we receive / check for welcome-file
		private void handleGet(File file) throws XPathExpressionException {
			// check if path is a directory or a file
			// if directory send page describing dir contents + links
			Monitor.write(this, "Getting file:"+file.getPath());
			
			// handling index.html
			Monitor.write(this,"Handling index.html");
			File[] fileList = file.listFiles();
			if(fileList != null) {
				for (File f : fileList) {
					if (parsedConfig.getWelcomeFile().equals(f.getName())) {
						file = f;
					}
				}
			}
			if (file.isDirectory()) {

				generateFileListPage(file);
				return;
			}
			
			// if file check specific types and handle seperatly
			Monitor.write(this, "Checking specific types");
			String fileName = file.getName();
			int dotPos = fileName.lastIndexOf('.');
			String fileExtension = fileName.substring((dotPos == -1) ? 0
					: dotPos + 1);
			for (Entry<String, String> entry : parsedConfig.getExtensions()
					.entrySet()) {
				if (fileExtension.equals(entry.getKey().toString())) {
					String line = request.getPath().substring(request.getPath().indexOf('?') + 1);
					if(request.getPath().indexOf('?') != -1){
						parseParams(line);
					}
					handleSpecificType(entry.getValue(), file);
					return;
				}
			}

			// if not specific send the html/html file as is with a 200 code
			// reading the file into a string and sending it!
			InputStream is;
			try {
				is = new FileInputStream(file);
			} catch (FileNotFoundException e) {
				e.printStackTrace();
				generateErrorPage(400);
				throw new RuntimeException(e.getMessage());
			}


			byte[] b = new byte[(int) file.length()];
			
			int offset = 0;
			int numRead = 0;
			try {
				while((offset < b.length) && 
						((numRead = is.read(b, offset, b.length-offset)) >= 0)){
					offset += numRead;
				}
			} catch (IOException e) {
				e.printStackTrace();
				generateErrorPage(500);
				throw new RuntimeException(e.getMessage());
			}
			
			
			StatusLine statusLine = new BasicStatusLine(HttpVersion.HTTP_1_1,
					200, "OK");

			
			Monitor.write(this, "Sending response");
			
			generateHttpResponseHtml(statusLine, b.clone(), false,
					null, parsedConfig.getMimeType(fileExtension));
			try {
				is.close();
			} catch (IOException e) {
				e.printStackTrace();
				generateErrorPage(500);
				throw new RuntimeException(e.getMessage());
			}

		}

		/**
		 * Sends to the user html
		 * 
		 * @param className
		 *            the specific type engine path
		 * @param file
		 *            the specific extension file we parse
		 */
		private void handleSpecificType(String className, File file) {

			try {
				// create the Properties file
				Properties p = new Properties();
				p.put("jre-path", parsedConfig.getCompilePath(className));

				// dynamically load engine
				Monitor.write(this, "Dynamically loading the engine");
				TypeHandler engine = (TypeHandler) Class.forName(className)
						.getConstructor(Properties.class).newInstance(p);

				// invoke handle method
				Monitor.write(this, "Invoking the handle method");
				ByteArrayOutputStream outArray = new ByteArrayOutputStream();
				PrintStream out = new PrintStream(outArray);
				String cookie = request.getHttpHeaders().get("cookie");

				Session session;
				if (cookie == null) {
					session = new Session();
				} else {
					cookie = cookie.split("=")[1].split(";")[0];
					Monitor.write(this,"Got the cookie:"+cookie);
					try {
						session = getSession(new Integer(cookie).intValue());
					} catch (Exception e) {
						session = new Session();
						cookie = null;
					}
					if (session == null) {
						generateErrorPage(400);
					}
				}

				Integer sessionId = null;
				engine.handle(file, params, out, session);
				if (session.toSet()) {
					sessionId = new Integer(getUniqueSessionId());
					putSession(session, sessionId);
				}

				out.flush();
				String output = new String(outArray.toByteArray());
				StatusLine statusLine = new BasicStatusLine(
						HttpVersion.HTTP_1_1, 200, "OK");
				generateHttpResponseHtml(statusLine, output.getBytes(), false,
						sessionId, "text/html");
				out.close();
				outArray.close();

			} catch (Exception e) {
				Monitor.write(this, e);
				Monitor.write(this, e.getStackTrace());
				generateErrorPage(500);
			}

		}

		private void parseParams(String line) {
			
			Monitor.write(this, "Parsing the parameters from line:"+line);
			String[] pairs = line.split("&");
			for (int i = 0; i < pairs.length; i++) {
				String[] pair = pairs[i].split("=");
				try {
					if (pair.length != 2) {
						generateErrorPage(400);
						throw new RuntimeException("Ilegal Query");
					}
					params.put(java.net.URLDecoder.decode(pair[0], "UTF-8"),
							java.net.URLDecoder.decode(pair[1], "UTF-8"));
				} catch (UnsupportedEncodingException e) {
					e.printStackTrace();
					generateErrorPage(400);
					throw new RuntimeException();
				}
			}
		}
	}

	/**
	 * 
	 * @author artem This class represents the sockets reader threads handler of
	 *         the server
	 * 
	 * 
	 */
	public class SocketReaderHandler implements Runnable {

		private Socket socket;
		private ThreadPool requestPool;

		/**
		 * provide the socket queue to wait on
		 */

		public SocketReaderHandler(Socket _socket, ThreadPool _threadPool) {
			socket = _socket;
			requestPool = _threadPool;
		}

		private void getHttpHeaders(Map<String, String> httpParams,
				HttpRequest httpRequest) {

			for (Header h : httpRequest.getAllHeaders()) {
				httpParams.put(h.getName().toLowerCase(), h.getValue()
						.toLowerCase());
			}

		}

		@Override
		public void run() {
			if (socket == null) {
				// there were no sockets waiting in the queue so we wait on it
				// again
				throw new RuntimeException("Null Socket");
			}

			DefaultHttpServerConnection serverConn = new DefaultHttpServerConnection();
			HttpParams params = new BasicHttpParams();
			HttpRequest httpRequest;
			String query = "";
			try {
				serverConn.bind(socket, params);
				serverConn.setSocketTimeout(50000);
				Monitor.write(this, "The socket has "+socket.getInputStream().available());
				httpRequest = serverConn.receiveRequestHeader();
				Monitor.write(this, "Received a request header: \n" + httpRequest.toString());
				if (httpRequest instanceof HttpEntityEnclosingRequest) {
					Monitor.write(this,"Got a post request");
					serverConn
							.receiveRequestEntity((HttpEntityEnclosingRequest) httpRequest);
					HttpEntity httpEntity = ((HttpEntityEnclosingRequest) httpRequest)
							.getEntity();
					while (httpEntity.getContent().available() > 0) {
						query += (char) httpEntity.getContent().read();
					}

				}

			} catch (HttpException e) {
				e.printStackTrace();
				throw new RuntimeException(e.getMessage());
			} catch (IOException e) {
				e.printStackTrace();
				throw new RuntimeException(e.getMessage());
			}
			Map<String, String> httpHeaders = new HashMap<String, String>();
			getHttpHeaders(httpHeaders, httpRequest);
			String url = httpRequest.getRequestLine().getUri();
			Monitor.write(this,"The received header is:"+url);
			String host = getHeaderValue(httpRequest, "HOST");
			int slashPos = url.indexOf("/");
			if (slashPos > -1)
				url = url.substring(slashPos);
			String httpMethod = httpRequest.getRequestLine().getMethod()
					.toUpperCase();
			Request request = new Request(socket, url, httpMethod, httpHeaders,
					query);
			try {
				requestPool.addTask(new RequestHandler(request));
			} catch (Exception e) {
				e.printStackTrace();
				throw new RuntimeException();
			}

		}

		/**
		 * This method returns the value of a specific header from our packet.
		 * If the header doesn't exist returns ""
		 */
		private String getHeaderValue(HttpMessage message, String name) {

			// removing white spaces in the header name and turning it to
			// uppercase
			String procName = "";
			StringTokenizer st = new StringTokenizer(name, " ", false);
			while (st.hasMoreElements())
				procName += st.nextToken();
			procName = procName.toUpperCase(Locale.ENGLISH);

			String value = "";
			for (Header header : message.getAllHeaders()) {
				String currHeaderName = header.getName().toUpperCase(
						Locale.ENGLISH);
				if (currHeaderName.equals(procName)) {
					value = header.getValue();
				}
			}

			return value.replaceAll(" ", "");
		}

	}

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

	private final Map<Integer, Session> sessionManager;
	private int sessionCounter = -1;
	private final Parser parsedConfig;
	private ServerSocket serverSocket = null;

	private ThreadPool socketPool = null;
	private ThreadPool requestPool = null;

	public WebServer(InputStream config) throws Exception {

		sessionManager = new HashMap<Integer, Session>();
		parsedConfig = new Parser(config);
		serverSocket = new ServerSocket();
	}

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

	/**
	 * This method initializes the objects that participate in the server. It
	 * initializes the thread pools and specific-type handlers.
	 * 
	 * @throws IOException
	 */
	public void init() throws IOException {

		try {
			// initializing the server socket to read on a specific port
			int port = parsedConfig.getPort();
			serverSocket = new ServerSocket(port);
			Monitor.write(this, "Creating a server that listens on port:"
					+ port);

			// initializing the thread pools
			int nSocketReaders = parsedConfig.getNumSocketReaders();
			int sQueueSize = parsedConfig.getNumSocketQueue();
			socketPool = new ThreadPool(nSocketReaders, sQueueSize);
			Monitor.write(this, "Initialized the socket thread pool with:"
					+ nSocketReaders + " threads and with this size of queue:"
					+ sQueueSize);

			int nRequestReaders = parsedConfig.getNumRequestReaders();
			int rQueueSize = parsedConfig.getNumRequestQueue();
			requestPool = new ThreadPool(nRequestReaders, rQueueSize);
			Monitor.write(this, "Initialized the socket thread pool with:"
					+ nRequestReaders + " threads and with this size of queue:"
					+ rQueueSize);

		} catch (XPathExpressionException e) {
			String msg = 	"An error has occured while initialzing the server. " +
							"Because of:\n"+e;
			Monitor.write(this,msg);
			System.err.println(msg);
			throw new IOException(msg);
		}

	}

	/**
	 * Fires off the server: listens endlessly for client connections.
	 */
	public void start() {

		// starting the threads in the thread pool
		socketPool.start();
		requestPool.start();

		// starting to listen for connections
		Socket clientSocket = null;

		while (true) {
			try {
				clientSocket = serverSocket.accept();
				Monitor.write(this, "Recieved a connection from the client");
				socketPool.addTask(new SocketReaderHandler(	clientSocket,
															requestPool));
			} catch (IOException e) {
				System.err.println("Error while accepting a connection");
			} catch (Exception fullQueueMessage) {
				// the queue of sockets is full and cann't receive connections
				
				// sending the err message to the client
				String errMessage = fullQueueMessage.getMessage();
				sendMessageToClient(clientSocket, errMessage);
			}
		}

	}

	public static void main(String[] args) throws Exception {
		FileInputStream config = new FileInputStream("config.xml");
		WebServer srv = new WebServer(config);
		srv.init();
		config.close();
		srv.start();
	}

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

	/**
	 * The message header we send includes the following headers:
	 * <ol>
	 * <li>The time of the response generation</li>
	 * <li>The content length of the response</li>
	 * <li>The mime type of the response content</li>
	 * <li>A statement that the server closes the connection upon termination</li>
	 * </ol>
	 * 
	 * @param clientSocket
	 * @param message
	 */
	private void sendMessageToClient(Socket clientSocket, String message) {

		String doctype = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional" +
						"//EN\"\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";
		String errMessage = doctype+"<html xmlns=\"http://www.w3.org/1999/xhtml\">" + "<head>"
				+ "<title>Service Unavailable</title>" + "</head>" + "<body>"
				+ message + "</body>" + "</html>";

		DefaultHttpServerConnection conn = null;
		try {
			// initialize socket
			HttpParams params = new BasicHttpParams();
			conn = new DefaultHttpServerConnection();
			conn.bind(clientSocket, params);

			// adding the relevant headers
			HttpResponse response = new BasicHttpResponse(	HttpVersion.HTTP_1_1,
															503, 
															"Service Unavailable");
			response.addHeader(HttpHeaders.DATE, new HttpDateGenerator().getCurrentDate());
			response.addHeader(HttpHeaders.CONTENT_TYPE, "text/html");
			response.addHeader(HttpHeaders.CONNECTION, "close");
			response.addHeader(HttpHeaders.CONTENT_LENGTH,new Integer(errMessage.length()).toString());

			// sending the message
			Monitor.write(this, "Sending the following error:" + errMessage);
			response.setEntity(new StringEntity(errMessage));
			conn.sendResponseHeader(response);
			conn.sendResponseEntity(response);
			conn.flush();

		} catch (IOException exit) {
			String msg = "A fatal error has occured while "
						+ "attempeting to send a message to the client.";
			System.err.println(msg);
			Monitor.write(this, msg);
			poolShutDown();
		} catch (HttpException ignore) { // couldn't happen
		} finally {
			socketShutDown(clientSocket, conn);
		}
	}

	/**
	 * Closing the sockets passed as parameters. If an error occurs while
	 * attempting to close a runtime exception will be thrown.
	 */
	private void socketShutDown(Socket clientSocket,
			DefaultHttpServerConnection conn) {

		Monitor.write(this, "Closing the clientSocket");
		try {
			if (conn != null) {
				conn.close();
				if (clientSocket != null) {
					clientSocket.close();
				}
			}
		} catch (IOException e) {
			String msg = "Unable to properly close the client socket.";
			System.err.println(msg);
			Monitor.write(this, msg);
			poolShutDown();
		}
	}

	/**
	 * Deactivating the threads in our thread pools
	 */
	private void poolShutDown() {

		Monitor.write(this, "Closing the thread pools");
		socketPool.shoutdown();
		requestPool.shoutdown();
	}

	private synchronized int getUniqueSessionId() {
		return ++sessionCounter;
	}

	/**
	 * Checks if their exists a Session for the user. If not it returns null.
	 * 
	 * @param user
	 *            the cookie that represents the user
	 * @return the session of the user
	 */
	private synchronized Session getSession(int user) {

		return sessionManager.get(user);
	}

	/**
	 * Creates a new session for a user
	 * 
	 * @return the user cookie
	 */
	private synchronized void putSession(Session session, int id) {
		sessionManager.put(id, session);
	}

////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

}
